﻿using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;

namespace EasyNet.TCP.Server
{
    /// <summary>
    /// 服务端核心抽象类
    /// </summary>
    abstract public class ServerCore
    {
        //非后备字段
        private readonly object[] lockers = new object[2];//线程锁
        //后备字段
        private string? _directory;
        private Encoding _encoding = Encoding.UTF8;
        private List<Client> _clients = new List<Client>();
        private ConcurrentDictionary<string, Client> _onlineClients = new ConcurrentDictionary<string, Client>();

        abstract protected void ClientConnected(Client client_);
        abstract protected void ByteArrayPKTHandled(Client client_, byte[] msg_);
        abstract protected void TextPKTHandled(Client client_, string msg_);
        abstract protected void FileInfoPKTHandled(Client client_, FileMsg file_msg);
        abstract protected void FilePKTHandled(Client client_, FileMsg file_msg);
        abstract protected void FileReceivingStarted(Client client_, FileMsg file_msg);
        abstract protected void FileReceived(Client client_, FileMsg file_msg);
        abstract protected void FilePKTSent(Client client_, FileMsg file_msg);
        abstract protected void FileSent(Client client_, FileMsg file_msg);
        abstract protected void ClientDisconnected(Client client_);
        abstract protected void ListeningException(Exception exception_);
        abstract protected void ReceivingException(Client client_, Exception exception_);
        abstract protected void HandlingTextPKTException(Client client_, Exception exception_);
        abstract protected void HandlingFileInfoPKTException(Client client_, Exception exception_);
        abstract protected void HandlingFilePKTException(Client client_, Exception exception_);
        abstract protected void DecryptingByteArrayPKTException(Client client_, Exception exception_);
        abstract protected void DecryptingTextPKTException(Client client_, Exception exception_);
        abstract protected void DecryptingFileInfoPKTException(Client client_, Exception exception_);
        abstract protected void DecryptingFilePKTException(Client client_, Exception exception_);
        abstract protected void FileSendingException(Client client_, Exception exception_);
        abstract protected byte[] ByteArrayPKTEncryptor(byte[] data_);
        abstract protected byte[] TextPKTEncryptor(byte[] data_);
        abstract protected byte[] FileInfoPKTEncryptor(byte[] data_);
        abstract protected byte[] FilePKTEncryptor(byte[] data_);
        abstract protected byte[] ByteArrayPKTDecryptor(byte[] data_);
        abstract protected byte[] TextPKTDecryptor(byte[] data_);
        abstract protected byte[] FileInfoPKTDecryptor(byte[] data_);
        abstract protected byte[] FilePKTDecryptor(byte[] data_);

        public void Start(string server_ip, int server_port)
        {
            IPAddress serverIP = IPAddress.Parse(server_ip);
            switch (serverIP.AddressFamily)
            {
                case AddressFamily.InterNetwork:
                    ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    break;
                case AddressFamily.InterNetworkV6:
                    ServerSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
                    break;
            }
            IP = server_ip;
            Port = server_port;
            IPEndPoint endPoint = new IPEndPoint(serverIP, server_port);
            ServerSocket.Bind(endPoint);
            ServerSocket.SendBufferSize = 10 * 1024 * 1024;
            ServerSocket.ReceiveBufferSize = 10 * 1024 * 1024;
            ServerSocket.Listen(10);
            Task.Run(() =>
            {
                while (true)
                {
                    Client client;
                    try
                    {
                        Socket clientSocket = ServerSocket.Accept();
                        client = new Client(clientSocket);
                        AddClient(client);
                    }
                    catch (Exception exception)
                    {
                        ListeningException(exception);
                        return;
                    }
                    ClientConnected(client);
                }
            });
        }
        public void Receive(Client client_)
        {
            Task.Run(() =>
            {
                FileStream? fileStream = null;
                while (true)
                {
                    if (!client_.Connected)
                    {
                        RemoveClient(client_);
                        ClientDisconnected(client_);
                        return;
                    }
                    if (client_.ClientSocket.Available == 0)
                    {
                        continue;
                    }
                    while (client_.ClientSocket.Available > 0)
                    {
                        byte[] type = new byte[1];
                        byte[] length = new byte[4];
                        byte[] data;
                        try
                        {
                            client_.ClientSocket.Receive(type, type.Length, SocketFlags.None);//接收包类型
                            client_.ClientSocket.Receive(length, length.Length, SocketFlags.None);//接收包长度
                            int data_length = BitConverter.ToInt32(length, 0);
                            data = new byte[data_length];
                            client_.ClientSocket.Receive(data, data.Length, SocketFlags.None);//接收包数据
                        }
                        catch (Exception exception)
                        {
                            ReceivingException(client_, exception);
                            return;
                        }
                        switch (type[0])
                        {
                            case (byte)PacketType.ByteArray:
                                try
                                {
                                    data = ByteArrayPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingByteArrayPKTException(client_, exception);
                                    continue;
                                }
                                ByteArrayPKTHandled(client_, data);
                                break;
                            case (byte)PacketType.Text:
                                try
                                {
                                    data = TextPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingTextPKTException(client_, exception);
                                    continue;
                                }
                                string msg;
                                try
                                {
                                    msg = System.Text.Encoding.UTF8.GetString(data);
                                }
                                catch (Exception exception)
                                {
                                    HandlingTextPKTException(client_, exception);
                                    continue;
                                }
                                TextPKTHandled(client_, msg);
                                break;
                            case (byte)PacketType.FileInfo:
                                try
                                {
                                    data = FileInfoPKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingFileInfoPKTException(client_, exception);
                                    continue;
                                }
                                FileMsg fileMsg;
                                try
                                {
                                    byte[] fileNameLengthBytes = data.Skip(0).Take(4).ToArray();
                                    int fileNameLength = BitConverter.ToInt32(fileNameLengthBytes);
                                    byte[] fileNameBytes = data.Skip(4).Take(fileNameLength).ToArray();
                                    string fileName = Encoding.UTF8.GetString(fileNameBytes);
                                    byte[] fileSizeBytes = data.Skip(4 + fileNameLength).Take(8).ToArray();
                                    long fileSize = BitConverter.ToInt64(fileSizeBytes);
                                    fileMsg = new FileMsg(client_.Directory, fileName, fileSize);
                                    client_._receivingFiles.Enqueue(fileMsg);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFileInfoPKTException(client_, exception);
                                    continue;
                                }
                                FileInfoPKTHandled(client_, fileMsg);
                                break;
                            case (byte)PacketType.File:
                                try
                                {
                                    data = FilePKTDecryptor(data);
                                }
                                catch (Exception exception)
                                {
                                    DecryptingFilePKTException(client_, exception);
                                    continue;
                                }
                                FileMsg fileMsg2;
                                try
                                {
                                    client_._receivingFiles.TryPeek(out fileMsg2);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFilePKTException(client_, exception);
                                    continue;
                                }
                                if (fileMsg2.TransmittedSize == 0)
                                {
                                    try
                                    {
                                        fileStream = new FileStream(client_.Directory + fileMsg2.FileName, FileMode.OpenOrCreate, FileAccess.Write);
                                    }
                                    catch (Exception exception)
                                    {
                                        HandlingFilePKTException(client_, exception);
                                        continue;
                                    }
                                    FileReceivingStarted(client_, fileMsg2);
                                }
                                try
                                {
                                    if (fileStream != null)
                                        fileStream.Write(data, 0, data.Length);
                                }
                                catch (Exception exception)
                                {
                                    HandlingFilePKTException(client_, exception);
                                    continue;
                                }
                                fileMsg2.TransmittedSize += data.Length;
                                FilePKTHandled(client_, fileMsg2);
                                if (fileMsg2.TransmittedSize == fileMsg2.FileSize)
                                {
                                    if (fileStream != null)
                                        fileStream.Close();
                                    client_._receivingFiles.TryDequeue(out fileMsg2);
                                    fileMsg2.TransmittedSize = 0;
                                    FileReceived(client_, fileMsg2);
                                }
                                break;
                        }
                    }
                }
            });
        }
        public void SendTextPKT(Client client_, string text_)
        {
            byte[] packet = Packet.CreateTextPKT(text_, client_.Encoding, TextPKTEncryptor);
            client_.ClientSocket.Send(packet, packet.Length, SocketFlags.None);
        }
        public void SendByteArrayPKT(Client client_, byte[] byte_array)
        {
            byte[] packet = Packet.CreateByteArrayPKT(byte_array, ByteArrayPKTEncryptor);
            client_.ClientSocket.Send(packet, packet.Length, SocketFlags.None);
        }
        public void SendFile(Client client_, string file_path, string text_msg)
        {
            FileMsg fileMsg;
            long fileSize;
            try
            {
                string fileName = Path.GetFileName(file_path);
                fileSize = new FileInfo(file_path).Length;
                byte[] fileInfoPacket = Packet.CreateFileInfoPKT(fileName, fileSize, FileInfoPKTEncryptor);
                client_.ClientSocket.Send(fileInfoPacket, fileInfoPacket.Length, SocketFlags.None);
                SendFileAsync(client_);
            }
            catch (Exception exception)
            {
                FileSendingException(client_, exception);
                return;
            }
            fileMsg = new FileMsg(file_path, fileSize);
            client_._sendingFiles.Enqueue(fileMsg);
        }
        private void SendFileAsync(Client client_)
        {
            Task.Run(() =>
            {
                while (client_._sendingFiles.TryPeek(out FileMsg fileMsg))
                {
                    FileStream fileStream = new FileStream(fileMsg.FilePath, FileMode.Open, FileAccess.Read);
                    byte[] file_bytes = new byte[1 * 1024 * 1024];
                    while (true)
                    {
                        try
                        {
                            int r = fileStream.Read(file_bytes, 0, file_bytes.Length);
                            if (r <= 0) break; //若是从流中读取完毕,则break;
                            byte[] filePacket;
                            if (r == file_bytes.Length)
                            {
                                filePacket = Packet.CreateFilePKT(file_bytes, FilePKTEncryptor);
                                client_.ClientSocket.Send(filePacket, filePacket.Length, SocketFlags.None);
                                fileMsg.TransmittedSize += file_bytes.Length;
                            }
                            else
                            {
                                byte[] leave_file_bytes = file_bytes.Skip(0).Take(r).ToArray();//最后剩下的文件字节数组
                                filePacket = Packet.CreateFilePKT(leave_file_bytes, FilePKTEncryptor);
                                client_.ClientSocket.Send(filePacket, filePacket.Length, SocketFlags.None);
                                fileMsg.TransmittedSize += leave_file_bytes.Length;
                            }
                        }
                        catch (Exception exception)
                        {
                            client_._sendingFiles.TryDequeue(out fileMsg);
                            fileStream.Close();
                            FileSendingException(client_, exception);
                            continue;
                        }
                        FilePKTSent(client_, fileMsg);
                    }
                    client_._sendingFiles.TryDequeue(out fileMsg);
                    fileStream.Close();
                    FileSent(client_, fileMsg);
                }
            });
        }
        private void AddClient(Client client_) 
        {
            lock (lockers[0]) 
            {
                _clients.Add(client_);
            }
        }
        private void RemoveClient(Client client_) 
        {
            lock (lockers[1])
            {
                _clients.Remove(client_);
            }
        } 
        public string IP { get; private set; }
        public int Port { get; private set; }
        public IReadOnlyList<Client> Clients 
        {
            get
            {
                return _clients;
            }
        }
        public IReadOnlyDictionary<string, Client> OnlineClients
        {
            get
            {
                return _onlineClients;
            }
        }
        private Socket ServerSocket { get; set; }
        public ServerCore()
        {
            for (int i = 0; i < lockers.Length; i++)
            {
                lockers[i] = new object();
            }
        }
    }
}
